/*
 * Copyright (c) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.huawei.healthecology.processor;

import com.huawei.healthecology.api.BleGatt;
import com.huawei.healthecology.data.ble.callback.BleCharacteristicValueChangeCallback;
import com.huawei.healthecology.data.ble.callback.BleConnectionChangeCallback;
import com.huawei.healthecology.data.ble.callback.BleMtuUpdatedCallback;
import com.huawei.healthecology.data.ble.callback.BleServiceDiscoveredCallback;
import com.huawei.healthecology.data.ble.callback.ReadBleCharacteristicValueCallback;
import com.huawei.healthecology.data.ble.callback.WriteBleCharacteristicValueCallback;
import com.huawei.healthecology.data.ble.data.BleCharacteristicData;
import com.huawei.healthecology.data.ble.data.BleCharacteristicProperty;
import com.huawei.healthecology.data.ble.data.BleConnectionPriority;
import com.huawei.healthecology.data.ble.data.BleDescriptorData;
import com.huawei.healthecology.data.ble.data.BleNotifyType;
import com.huawei.healthecology.data.ble.data.BleProfileUuid;
import com.huawei.healthecology.data.ble.data.BleServiceData;
import com.huawei.healthecology.data.ble.data.CharacteristicConstant;
import com.huawei.healthecology.data.ble.data.GattDescriptorConstants;
import com.huawei.healthecology.data.ble.request.BleConnectionPriorityRequest;
import com.huawei.healthecology.data.ble.request.BleDeviceCharacteristicGetRequest;
import com.huawei.healthecology.data.ble.request.BleDeviceConnectRequest;
import com.huawei.healthecology.data.ble.request.BleDeviceConnectionCloseRequest;
import com.huawei.healthecology.data.ble.request.BleMtuSetRequest;
import com.huawei.healthecology.data.ble.request.CharacteristicNotifyRequest;
import com.huawei.healthecology.data.ble.request.CharacteristicReadRequest;
import com.huawei.healthecology.data.ble.request.CharacteristicWriteRequest;
import com.huawei.healthecology.data.ble.request.DescriptorReadRequest;
import com.huawei.healthecology.data.ble.request.DescriptorWriteRequest;
import com.huawei.healthecology.data.ble.response.BleResponseCode;
import com.huawei.healthecology.data.ble.response.ConnectionPriorityResponse;
import com.huawei.healthecology.data.ble.response.DeviceCharacteristicGetResponse;
import com.huawei.healthecology.data.ble.response.DeviceServiceGetResponse;
import com.huawei.healthecology.data.utils.BluetoothProfileByteUtil;
import com.huawei.healthecology.data.utils.CallbackProvider;
import com.huawei.healthecology.data.utils.OptionalX;
import com.huawei.healthecology.data.utils.StringUtils;
import com.huawei.healthecology.log.LogUtil;

import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import ohos.bluetooth.ble.BlePeripheralDevice;
import ohos.bluetooth.ble.GattCharacteristic;
import ohos.bluetooth.ble.GattDescriptor;
import ohos.bluetooth.ble.GattService;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Ble gatt Processor
 */
@RequiredArgsConstructor(staticName = "get")
public class BleGattProcessor implements BleGatt, HealthEcologyProcessor {
    private static final String TAG = "BleGattProcessor";

    private final List<BlePeripheralDevice> connectedDevices = new ArrayList<>();

    private GattEventMonitor eventMonitor;

    @Override
    public void initProcessor() {
        this.eventMonitor = GattEventMonitor.get();
    }

    @Override
    public void releaseResource() {
        connectedDevices.forEach(BlePeripheralDevice::disconnect);
        connectedDevices.forEach(BlePeripheralDevice::close);
        Optional.ofNullable(eventMonitor).ifPresent(GattEventMonitor::clearAllCallback);
    }

    @Override
    public void destroyProcessor() {
        this.connectedDevices.clear();
        this.eventMonitor = null;
    }

    @Override
    public BleResponseCode createBleConnection(@NonNull BleDeviceConnectRequest request) {
        getConnectedDeviceById(request.getDeviceId()).ifPresent(device -> {
            connectedDevices.removeIf(item -> item.getDeviceAddr().equalsIgnoreCase(device.getDeviceAddr()));
            device.close();
        });
        Optional<BlePeripheralDevice> peripheralDevice = (validateMacAddress(request.getDeviceId()))
            ? Optional.of(BlePeripheralDevice.createInstance(request.getDeviceId()))
            : Optional.empty();
        peripheralDevice.ifPresent(connectedDevices::add);
        return peripheralDevice
            .map(device -> device.connect(request.isAutoConnect(), eventMonitor.getBlePeripheralCallback(device)))
            .map(result -> result ? BleResponseCode.OPERATION_SUCCESS : BleResponseCode.CREATE_CONNECTION_FAILED)
            .orElse(BleResponseCode.DEVICE_NOT_FOUND);
    }

    @Override
    public BleResponseCode closeBleConnection(@NonNull BleDeviceConnectionCloseRequest closeRequest) {
        Optional<BlePeripheralDevice> peripheralDevice = getConnectedDeviceById(closeRequest.getDeviceId());
        peripheralDevice.ifPresent(device ->
            connectedDevices.removeIf(item -> item.getDeviceAddr().equalsIgnoreCase(device.getDeviceAddr())));
        return peripheralDevice
            .map(device -> device.disconnect() && device.close())
            .map(result -> result ? BleResponseCode.OPERATION_SUCCESS : BleResponseCode.CLOSE_CONNECTION_FAILED)
            .orElse(BleResponseCode.DEVICE_NOT_FOUND);
    }

    @Override
    public void onBleConnectionChange(@NonNull BleConnectionChangeCallback changeCallback) {
        OptionalX.ofNullable(eventMonitor.getConnectionChangeProvider())
            .ifPresent(callbackProvider -> callbackProvider.add(changeCallback))
            .ifNotPresent(() -> eventMonitor.setConnectionChangeProvider(
                CallbackProvider.<BleConnectionChangeCallback>builder().callback(changeCallback).build()));
    }

    @Override
    public void onBleServiceDiscovered(BleServiceDiscoveredCallback discoveredCallback) {
        OptionalX.ofNullable(eventMonitor.getServiceDiscoveredProvider())
            .ifPresent(callbackProvider -> callbackProvider.add(discoveredCallback))
            .ifNotPresent(() -> eventMonitor.setServiceDiscoveredProvider(
                CallbackProvider.<BleServiceDiscoveredCallback>builder().callback(discoveredCallback).build()));
    }

    @Override
    public void onBleMtuUpdated(BleMtuUpdatedCallback mtuUpdatedCallback) {
        OptionalX.ofNullable(eventMonitor.getMtuUpdatedProvider())
            .ifPresent(callbackProvider -> callbackProvider.add(mtuUpdatedCallback))
            .ifNotPresent(() -> eventMonitor.setMtuUpdatedProvider(
                CallbackProvider.<BleMtuUpdatedCallback>builder().callback(mtuUpdatedCallback).build()));
    }

    @Override
    public void onBleCharacteristicValueRead(ReadBleCharacteristicValueCallback readCallback) {
        OptionalX.ofNullable(eventMonitor.getReadCharacteristicProvider())
            .ifPresent(callbackProvider -> callbackProvider.add(readCallback))
            .ifNotPresent(() -> eventMonitor.setReadCharacteristicProvider(
                CallbackProvider.<ReadBleCharacteristicValueCallback>builder().callback(readCallback).build()));
    }

    @Override
    public void onBleCharacteristicValueWrite(WriteBleCharacteristicValueCallback writeCallback) {
        OptionalX.ofNullable(eventMonitor.getWriteCharacteristicProvider())
            .ifPresent(callbackProvider -> callbackProvider.add(writeCallback))
            .ifNotPresent(() -> eventMonitor.setWriteCharacteristicProvider(
                CallbackProvider.<WriteBleCharacteristicValueCallback>builder().callback(writeCallback).build()));
    }

    @Override
    public void onBleCharacteristicValueChange(BleCharacteristicValueChangeCallback changeCallback) {
        OptionalX.ofNullable(eventMonitor.getCharacteristicValueChangeProvider())
            .ifPresent(callbackProvider -> callbackProvider.add(changeCallback))
            .ifNotPresent(() -> eventMonitor.setCharacteristicValueChangeProvider(
                CallbackProvider.<BleCharacteristicValueChangeCallback>builder().callback(changeCallback).build()));
    }

    @Override
    public BleResponseCode readBleCharacteristicValue(@NonNull CharacteristicReadRequest readRequest) {
        return getConnectedDeviceById(readRequest.getDeviceId())
            .map(device -> getCharacteristicById(device, readRequest.getServiceId(), readRequest.getCharacteristicId())
                .map(device::readCharacteristic)
                .map(result -> result ? BleResponseCode.OPERATION_SUCCESS : BleResponseCode.SYSTEM_ERROR)
                .orElse(BleResponseCode.INVALID_SERVICE_OR_CHARACTERISTIC))
            .orElse(BleResponseCode.NOT_INITIALIZED);
    }

    @Override
    public BleResponseCode writeBleCharacteristicValue(@NonNull CharacteristicWriteRequest request) {
        Optional<GattCharacteristic> gattCharacteristic = getConnectedDeviceById(request.getDeviceId())
            .flatMap(device -> getCharacteristicById(device, request.getServiceId(), request.getCharacteristicId()));
        return getConnectedDeviceById(request.getDeviceId())
            .map(device -> gattCharacteristic
                .flatMap(characteristic -> withWriteCharacteristic(characteristic, request.getCharacteristicData()))
                .map(device::writeCharacteristic)
                .map(result -> result ? BleResponseCode.OPERATION_SUCCESS : BleResponseCode.SYSTEM_ERROR)
                .orElse(BleResponseCode.INVALID_SERVICE_OR_CHARACTERISTIC))
            .orElse(BleResponseCode.NOT_INITIALIZED);
    }

    @Override
    public BleResponseCode notifyBleCharacteristicValueChange(CharacteristicNotifyRequest request) {
        LogUtil.info(TAG, "notifyBleCharacteristicValueChange.");
        int propertyFlag = BleNotifyType.NOTIFICATION.equals(request.getNotifyType())
            ? CharacteristicConstant.NOTIFY_PROPERTY.getValue()
            : CharacteristicConstant.INDICATE_PROPERTY.getValue();
        Optional<GattCharacteristic> gattCharacteristic = getConnectedDeviceById(request.getDeviceId())
            .flatMap(device -> getCharacteristicById(device, request.getServiceId(), request.getCharacteristicId()));
        return getConnectedDeviceById(request.getDeviceId())
            .map(device -> gattCharacteristic
                .filter(characteristic -> (characteristic.getProperties() & propertyFlag) != 0)
                .map(characteristic -> toEnableCharacteristicNotify(device, characteristic, request.getNotifyType()))
                .map(result -> result ? BleResponseCode.OPERATION_SUCCESS : BleResponseCode.SYSTEM_ERROR)
                .orElse(BleResponseCode.INVALID_SERVICE_OR_CHARACTERISTIC))
            .orElse(BleResponseCode.NOT_INITIALIZED);
    }

    @Override
    public BleResponseCode readBleDescriptorValue(DescriptorReadRequest request) {
        Optional<GattCharacteristic> gattCharacteristic = getConnectedDeviceById(request.getDeviceId())
            .flatMap(device -> getCharacteristicById(device, request.getServiceId(), request.getCharacteristicId()));
        return getConnectedDeviceById(request.getDeviceId())
            .map(device -> gattCharacteristic
                .map(characteristic -> getDescriptorById(characteristic, request.getDescriptorId()))
                .map(gattDescriptor -> gattDescriptor
                    .map(device::readDescriptor)
                    .map(result -> result ? BleResponseCode.OPERATION_SUCCESS : BleResponseCode.SYSTEM_ERROR)
                    .orElse(BleResponseCode.INVALID_CHARACTERISTIC_DESCRIPTOR))
                .orElse(BleResponseCode.INVALID_SERVICE_OR_CHARACTERISTIC))
            .orElse(BleResponseCode.NOT_INITIALIZED);
    }

    @Override
    public BleResponseCode writeBleDescriptorValue(DescriptorWriteRequest writeRequest) {
        Optional<GattCharacteristic> gattCharacteristic = getConnectedDeviceById(
            writeRequest.getDescriptorData().getDeviceId()).flatMap(device -> getCharacteristicById(device,
            writeRequest.getDescriptorData().getServiceId(),
            writeRequest.getDescriptorData().getCharacteristicId()));
        String descriptorId = Optional.ofNullable(writeRequest.getDescriptorData())
            .map(BleDescriptorData::getDescriptorId)
            .orElse(null);
        String descriptorData = Optional.ofNullable(writeRequest.getDescriptorData())
            .map(BleDescriptorData::getCharacteristicData)
            .orElse(null);
        return getConnectedDeviceById(writeRequest.getDescriptorData().getDeviceId())
            .map(device -> gattCharacteristic
                .map(characteristic -> getDescriptorById(characteristic, descriptorId))
                .map(gattDescriptor -> gattDescriptor
                    .flatMap(descriptor -> withWriteDescriptor(descriptor, descriptorData))
                    .map(device::writeDescriptor)
                    .map(result -> result ? BleResponseCode.OPERATION_SUCCESS : BleResponseCode.SYSTEM_ERROR)
                    .orElse(BleResponseCode.INVALID_CHARACTERISTIC_DESCRIPTOR))
                .orElse(BleResponseCode.INVALID_SERVICE_OR_CHARACTERISTIC))
            .orElse(BleResponseCode.NOT_INITIALIZED);
    }

    @NonNull
    @Override
    public ConnectionPriorityResponse setConnectionPriority(BleConnectionPriorityRequest priorityRequest) {
        BleConnectionPriority connectionPriority = Optional.ofNullable(priorityRequest)
            .map(BleConnectionPriorityRequest::getConnectionPriority)
            .orElse(BleConnectionPriority.NORMAL_PRIORITY);
        return getConnectedDeviceById(priorityRequest.getDeviceId())
            .map(device -> device.requestBleConnectionPriority(connectionPriority.ordinal()))
            .map(connected -> ConnectionPriorityResponse.builder()
                .responseCode(BleResponseCode.OPERATION_SUCCESS)
                .deviceId(priorityRequest.getDeviceId())
                .priority(connectionPriority)
                .build())
            .orElse(ConnectionPriorityResponse.builder()
                .responseCode(BleResponseCode.OPERATION_FAILED)
                .deviceId(priorityRequest.getDeviceId())
                .build());
    }

    @Override
    public DeviceServiceGetResponse getBleDeviceServices(@NonNull String deviceId) {
        return getConnectedDeviceById(deviceId)
            .map(device -> device.getServices().stream())
            .map(gattServiceStream -> gattServiceStream
                .map(gattService -> Optional.ofNullable(gattService.getUuid())
                    .map(uuid -> BleServiceData.of(uuid.toString(), gattService.isPrimary()))
                    .orElse(BleServiceData.of("", gattService.isPrimary())))
                .collect(Collectors.toList()))
            .map(dataList -> DeviceServiceGetResponse.builder()
                .deviceId(deviceId)
                .bleServiceDataList(dataList)
                .responseCode(BleResponseCode.OPERATION_SUCCESS)
                .build())
            .orElse(DeviceServiceGetResponse.builder()
                .deviceId(deviceId)
                .responseCode(BleResponseCode.DEVICE_NOT_FOUND)
                .build());
    }

    @Override
    public DeviceCharacteristicGetResponse getBleDeviceCharacteristics(BleDeviceCharacteristicGetRequest request) {
        return getConnectedDeviceById(request.getDeviceId())
            .filter(device -> !StringUtils.isEmpty(request.getServiceId()))
            .flatMap(device -> device.getService(UUID.fromString(request.getServiceId())))
            .map(gattService -> gattService.getCharacteristics().stream())
            .map(characteristicStream -> characteristicStream
                .filter(characteristic -> Optional.ofNullable(characteristic.getUuid()).isPresent())
                .map(characteristic -> BleCharacteristicData.builder()
                    .characteristicId(characteristic.getUuid().toString())
                    .characteristicProperty(getProperty(characteristic))
                    .build())
                .collect(Collectors.toList()))
            .map(dataList -> DeviceCharacteristicGetResponse.builder()
                .dataList(dataList)
                .deviceId(request.getDeviceId())
                .responseCode(BleResponseCode.OPERATION_SUCCESS)
                .build())
            .orElse(DeviceCharacteristicGetResponse.builder()
                .deviceId(request.getDeviceId())
                .responseCode(BleResponseCode.INVALID_SERVICE_UUID)
                .build());
    }

    @Override
    public BleResponseCode setBleMtu(BleMtuSetRequest request) {
        return getConnectedDeviceById(request.getDeviceId())
            .map(device -> device.requestBleMtuSize(request.getMtuValue()))
            .map(result -> result ? BleResponseCode.OPERATION_SUCCESS : BleResponseCode.SYSTEM_ERROR)
            .orElse(BleResponseCode.NOT_INITIALIZED);
    }

    private boolean validateMacAddress(String address) {
        String macRegex = "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$";
        Pattern pattern = Pattern.compile(macRegex);
        return Optional.ofNullable(address)
            .map(pattern::matcher)
            .map(Matcher::matches)
            .orElse(false);
    }

    private Optional<BlePeripheralDevice> getConnectedDeviceById(@NonNull String deviceId) {
        return connectedDevices.stream()
            .filter(item -> deviceId.equalsIgnoreCase(item.getDeviceAddr()))
            .findAny();
    }

    private Optional<GattCharacteristic> getCharacteristicById(BlePeripheralDevice peripheralDevice,
        String serviceId, String characteristicId) {
        Optional<GattService> gattService = Optional.ofNullable(peripheralDevice)
            .filter(device -> !StringUtils.isEmptySequences(serviceId, characteristicId))
            .flatMap(device -> device.getService(UUID.fromString(serviceId)));
        return gattService.flatMap(service -> service.getCharacteristic(UUID.fromString(characteristicId)));
    }

    private Optional<GattDescriptor> getDescriptorById(GattCharacteristic gattCharacteristic, String descriptorId) {
        return Optional.ofNullable(gattCharacteristic)
            .filter(characteristic -> !StringUtils.isEmpty(descriptorId))
            .flatMap(characteristic -> characteristic.getDescriptor(UUID.fromString(descriptorId)));
    }

    private Optional<GattCharacteristic> withWriteCharacteristic(GattCharacteristic gattCharacteristic,
        String characteristicData) {
        return Optional.ofNullable(gattCharacteristic)
            .filter(characteristic -> !StringUtils.isEmpty(characteristicData))
            .filter(characteristic -> characteristic.setValue(BluetoothProfileByteUtil.hexToBytes(characteristicData)));
    }

    private Optional<GattDescriptor> withWriteDescriptor(GattDescriptor gattDescriptor, String descriptorData) {
        return Optional.ofNullable(gattDescriptor)
            .filter(descriptor -> !StringUtils.isEmpty(descriptorData))
            .filter(descriptor -> descriptor.setValue(BluetoothProfileByteUtil.hexToBytes(descriptorData)));
    }

    private boolean toEnableCharacteristicNotify(BlePeripheralDevice peripheralDevice,
        GattCharacteristic gattCharacteristic, BleNotifyType notifyType) {
        LogUtil.info(TAG, "enableCharacteristicNotifyOrIndicate: notifyTpe = " + notifyType);
        byte[] descriptorValue = BleNotifyType.NOTIFICATION.equals(notifyType)
            ? GattDescriptorConstants.getEnableNotificationValue()
            : GattDescriptorConstants.getEnableIndicationValue();
        return Optional.ofNullable(peripheralDevice)
            .map(device -> Optional.ofNullable(gattCharacteristic)
                .filter(characteristic -> device.setNotifyCharacteristic(characteristic, true))
                .flatMap(characteristic -> characteristic
                    .getDescriptor(BleProfileUuid.CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR.getUuid()))
                .filter(descriptor -> descriptor.setValue(descriptorValue))
                .map(device::writeDescriptor)
                .orElse(false))
            .orElse(false);
    }

    private BleCharacteristicProperty getProperty(GattCharacteristic characteristic) {
        boolean isReadable =
            ((characteristic.getProperties() & CharacteristicConstant.READ_PROPERTY.getValue()) != 0);
        boolean isWriteable =
            ((characteristic.getProperties() & CharacteristicConstant.WRITE_PROPERTY.getValue()) != 0);
        boolean isNotifySupported =
            ((characteristic.getProperties() & CharacteristicConstant.NOTIFY_PROPERTY.getValue()) != 0);
        boolean isIndicateSupported =
            ((characteristic.getProperties() & CharacteristicConstant.INDICATE_PROPERTY.getValue()) != 0);
        return BleCharacteristicProperty.of(isReadable, isWriteable, isNotifySupported, isIndicateSupported);
    }
}
